/*
 *   The contents of this file are subject to the Mozilla Public License
 *   Version 1.1 (the "License"); you may not use this file except in
 *   compliance with the License. You may obtain a copy of the License at
 *   http://www.mozilla.org/MPL/
 *
 *   Software distributed under the License is distributed on an "AS IS"
 *   basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 *   License for the specific language governing rights and limitations
 *   under the License.
 *
 *   The Original Code is Matra - the DTD Parser.
 *
 *   The Initial Developer of the Original Code is Conrad S Roche.
 *   Portions created by Conrad S Roche are Copyright (C) Conrad 
 *   S Roche. All Rights Reserved.
 *
 *   Alternatively, the contents of this file may be used under the terms
 *   of the GNU GENERAL PUBLIC LICENSE Version 2 or any later version
 *   (the  "[GPL] License"), in which case the
 *   provisions of GPL License are applicable instead of those
 *   above.  If you wish to allow use of your version of this file only
 *   under the terms of the GPL License and not to allow others to use
 *   your version of this file under the MPL, indicate your decision by
 *   deleting  the provisions above and replace  them with the notice and
 *   other provisions required by the GPL License.  If you do not delete
 *   the provisions above, a recipient may use your version of this file
 *   under either the MPL or the GPL License."
 *
 *   [NOTE: The text of this Exhibit A may differ slightly from the text of
 *   the notices in the Source Code files of the Original Code. You should
 *   use the text of this Exhibit A rather than the text found in the
 *   Original Code Source Code for Your Modifications.]
 *
 * Created: Conrad S Roche <derupe at users.sourceforge.net>,  25-Jul-2000
 */

package com.conradroche.matra.decl;

import java.util.ArrayList;

import com.conradroche.dtd.decl.AttDef;
import com.conradroche.matra.data.*;
import com.conradroche.matra.exception.DTDSyntaxException;

/**
 * Class to hold an Attribute declaration.
 * 
 * @author Conrad Roche
 */
public class Attribute {
	
	/**
	 * The name of the attribute.
	 */
	private String name;
	
	/**
	 * The name of the element type to which
	 * this attribute is associated. 
	 */
	private String eleName;

	/**
	 * Value of getAttType() if the Attribute defined is
	 * of Notation Type.
	 */	
	public static final int TYPE_NOTATION = 3;

	/**
	 * Value of getAttType() if the Attribute defined is
	 * an Enumeration.
	 */	
	public static final int TYPE_ENUMERATION = 4;
	
	/**
	 * The type of the attribute. This can take any of the four values -
	 * <code>TYPE_STRING</code>, <code>TYPE_TOKENIZED</code>, 
	 * <code>TYPE_NOTATION</code> or <code>TYPE_ENUMERATION</code>.
	 */
	private int attType;

	/**
	 * The data type for this attribute.
	 */	
	private String dataType;
	
	/**
	 * The default value for this attribute.
	 */
	private String defaultValue;
	
	/**
	 * The enumerated list of values for the attribute.
	 * This will be populated only if the type of the attribute
	 * is of type <code>TYPE_ENUMERATION</code> or of type
	 * <code>TYPE_NOTATION</code>.
	 */
	private String[] enumeration;

	/**
	 * Constant indicating that the attribute is a
	 * mandatory attribute.
	 */
	public static final String OPTIONALITY_STR_REQUIRED = "#REQUIRED";

	/**
	 * Constant indicating that the attribute is an
	 * optional attribute.
	 */
	public static final String OPTIONALITY_STR_IMPLIED  = "#IMPLIED";

	/**
	 * Constant indicating that the attribute is a
	 * fixed-value attribute.
	 */
	public static final String OPTIONALITY_STR_FIXED    = "#FIXED";
	
	/**
	 * The "optionality" of this attribute.
	 */
	private int optionality;

	/**
	 * Constant string denoting the token "NOTATION".
	 */
	public static final String NOTATION = "NOTATION"; 

	/**
	 * Constant string denoting the token "CDATA".
	 */
	public static final String CDATA    = "CDATA";


	/**
	 * Constant string denoting the token "ID".
	 */
	private static final String ID = "ID";
	
	/**
	 * List of valid tokenized types.
	 */
	private static final String[] tokenizedTypes 
		= { ID, "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"};

/**
 * Attribute constructor.
 */
public Attribute() {
	super();
}

/**
 * Attribute Constructor for attributes of type Enumeration.
 * 
 * @param elementName The Element name.
 * @param attrName The name of the attribute.
 * @param enums The enumeration for the attribute.
 * @param defaultDecl The default declaration for the attribute.
 */
public Attribute(String elementName, String attrName, String[] enums, String defaultDecl) {
	
	eleName	= elementName;
	name = attrName;
	
	attType = Attribute.TYPE_ENUMERATION;
	
	this.defaultValue = defaultDecl;

	enumeration = enums;
}

/**
 * Attribute constructor.
 * 
 * @param elementName The Element name.
 * @param attrName The name of the attribute.
 * @param attributeType The type of the attribute.
 * @param dataType The data type of the attribute.
 * @param defaultDecl The default declaration for the attribute.
 */
public Attribute(String elementName, String attrName, int attributeType, String dataType, String defaultDecl) {
	
	eleName	= elementName;
	name = attrName;
	attType = attributeType;
	this.dataType = dataType;
	this.defaultValue = defaultDecl;
}

/**
 * Return the type of the Attribute.
 * 
 * @return The attribute type.
 */
public int getAttType() {
	return attType;
}

/**
 * Get the data type of the Attribute.
 * 
 * @see #setDataType
 * 
 * @return The attribute datatype.
 */
public String getDataType() {
	return dataType;
}

/**
 * Get the Default declaration of the Attribute.
 * 
 * @see #setDefaultValue
 * 
 * @return The attribute default declaration.
 */
public String getDefaultValue() {
	
	if(getOptionality() == AttDef.OPTIONALITY_DEFAULT) {
		return defaultValue;
	}
	
	return null;
}

/**
 * Returns the fixed value for the attribute.
 * Returns null if the attribute does not have
 * a fixed values.
 * 
 * @return The fixed value for the attribute.
 */
public String getFixedValue() {
	
	if(getOptionality() == AttDef.OPTIONALITY_FIXED) {
		return defaultValue;
	}
	
	return null;
}

/**
 * If the Attribute is of Enumerated type, get the list of permitted values.
 * 
 * @return The list of permintted values.
 */
public String[] getEnumeratedValues() {
	return enumeration;
}

/**
 * Get the name of the attribute.
 * 
 * @return The attribute name.
 * 
 * @see #setAttributeName
 */
public String getAttributeName() {
	return name;
}

/**
 * Get the optionality of the attribute.
 * 
 * @see #setOptionality
 * 
 * @return The attribute optionality.
 */
public int getOptionality() {
	return optionality;
}

/**
 * Get the optionality of the attribute.
 * 
 * @return The attribute optionality.
 */
public String getOptionalityString() {
	
	//CR: TODO: Remove this method? Or make it private
	if(optionality == AttDef.OPTIONALITY_REQUIRED) {
		return OPTIONALITY_STR_REQUIRED;
	} else if(optionality == AttDef.OPTIONALITY_IMPLIED) {
		return OPTIONALITY_STR_IMPLIED;
	} else if(optionality == AttDef.OPTIONALITY_FIXED) {
		return OPTIONALITY_STR_FIXED;
	}
	
	return "";
}

/**
 * Check if the Attribute is of NOTATION type.
 * 
 * @return <code>true</code> if the attribute is a Notation type.
 * 			<code>false</code> if the attribute is not.
 */
public boolean isNotationType() {

	if(getAttType() ==  Attribute.TYPE_NOTATION) {
		return true;
	}

	return false;
}

/**
 * Checks the data type for this attribute
 * and determines its attribute type.
 * 
 * @param token The data type for this attribute.
 */
private void checkAttType(String token) {
	
	if( token.equals(CDATA) ) {
		
		setDataType(token);
		setAttrType(AttDef.TYPE_STRING);
		
	} else if( checkIfTokenizedType(token) ) {
		
		setDataType(token);
		setAttrType(AttDef.TYPE_TOKENIZED);
		
	} else if( token.equals(NOTATION) ) { //NotationType
		
		setAttrType(Attribute.TYPE_NOTATION);
		
	} else { //Enumeration
		
		setAttrType(Attribute.TYPE_ENUMERATION);
	}
}

/**
 * Check if the Attribute is of STRING type (i.e. CDATA).
 * 
 * @return <code>true</code> if the attribute is string type;
 * <code>false</code> otherwise.
 */
public boolean isStringType() {

	if(getAttType() ==  AttDef.TYPE_STRING) {
		return true;
	}

	return false;
}

/**
 * Checks if the attribute is of type enumeration.
 * 
 * @return <code>true</code> the attribute is of type 
 * enumeration; <code>false</code> otherwise.
 */
public boolean isEnumeration() {
	
	if(getAttType() ==  Attribute.TYPE_ENUMERATION) {
		return true;
	}

	return false;
}

/**
 * Check if the Attribute is a Tokenized type.
 * 
 * @return <code>true</code> if this attribute is
 * 		of tokenized type; <code>false</code> otherwise.
 * 
 * @param token The data type for this attribute.
 */
private boolean checkIfTokenizedType(String token) {

/*
[56] TokenizedType ::= 'ID' | 'IDREF' | 'IDREFS' | 'ENTITY' | 'ENTITIES' | 'NMTOKEN' | 'NMTOKENS' 
*/

	for(int i = 0; i < tokenizedTypes.length; i++) {
		if(token.equals(tokenizedTypes[i])) {
			return true;
		}
	}

	return false;
}

/**
 * Reads the default declaration for the attribute.
 * 
 * @param AttDef The data stream to read from.
 */
private void readDefaultDecl(DTDData attDef) {

	/*
	[60] DefaultDecl ::= '#REQUIRED' | '#IMPLIED' | (('#FIXED' S)? AttValue)
	[10] AttValue ::= '"' ([^<&"] | Reference)* '"' | "'" ([^<&'] | Reference)* "'"

	[67] Reference ::= EntityRef | CharRef
	[68] EntityRef ::= '&' Name ';'
	[66] CharRef ::= '&#' [0-9]+ ';' | '&#x' [0-9a-fA-F]+ ';'
	*/
	
	String defaultDecl = null;
	
	//get the DefaultDecl
	attDef.skipWhiteSpace();

	char nextChar = attDef.lookNextChar();
	
	if( (nextChar == '\'') || (nextChar == '\"') ) {
		
		//set the optionality
		optionality = AttDef.OPTIONALITY_DEFAULT;
		
		char[] defaultDeclDelim = { attDef.getNextChar() }; // this will be either ' or "
		
		defaultDecl = attDef.getNextToken(defaultDeclDelim);
		setDefaultValue(defaultDecl);

	} else if(nextChar == '#') {
		String optionality = attDef.getNextToken(); //'#REQUIRED' | '#IMPLIED'| '#FIXED'
		setOptionality(optionality);
		
		if( optionality.equals("#FIXED") ) {
			attDef.skipWhiteSpace();
			//CR: TODO: check for error - if neither ' or " 
			char[] defaultDeclDelim = { attDef.getNextChar() }; // this should be either ' or "
			defaultDecl = attDef.getNextToken(defaultDeclDelim);
			
			setDefaultValue(defaultDecl);
		}
	}
	//CR: TODO: check for error - if neither ', " and # ... throw syntax error
}

/**
 * Read the permitted values for the Enumerated type.<p/>
 * Parses the data passed based on the following grammar - <br/>
 * <code>
 * [59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'
 * </code>
 * 
 * @param enums DTDData containing the permitted values in dtd syntax.
 *  
 * @return A String array containing the list of permitted values.
 */
private String[] readEnums(DTDData enums) throws DTDSyntaxException {


	ArrayList enumeration = new ArrayList();
	
//	[59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')'

	enums.skipWhiteSpace();
	if(enums.checkNextChar('(') == Data.CHAR_NOT_FOUND) {
		//CR: TODO: throw exception 
	}
	enums.skipWhiteSpace();

	String nmToken = enums.getNextNmToken();
	
	if(nmToken == null) {
		//CR: TODO: throw exception
		//there should be at least one value in an enumeration
	}

	while(nmToken != null) {
		enumeration.add(nmToken);
		enums.skipWhiteSpace();
		
		if(enums.checkNextChar(')') == Data.FOUND_CHAR) {
			return (String[]) enumeration.toArray(new String[0]);
		} else if(enums.checkNextChar('|') == Data.FOUND_CHAR) {
			enums.skipWhiteSpace();
			nmToken = enums.getNextNmToken();
		} else {
			//invalid char!
			throw new DTDSyntaxException("Got invalid char (" + enums.lookNextChar() + 
						") while reading enums for attribute " + getAttributeName());
		}
	}
	
	return (String[]) enumeration.toArray(new String[0]);
}

/**
 * Read the attribute details from the given data.
 * 
 * @param AttDef The attribute definition to parse.
 * 
 * @throws DTDSyntaxException If it encounters a 
 * syntax exception.
 */
public void readNextAttribute(DTDData AttDef) throws DTDSyntaxException {

/*
[53] AttDef ::= S Name S AttType S DefaultDecl

[54] AttType ::= StringType | TokenizedType | EnumeratedType
[55] StringType ::= 'CDATA'
[56] TokenizedType ::= 'ID' | 'IDREF' | 'IDREFS' | 'ENTITY' | 'ENTITIES' | 'NMTOKEN' | 'NMTOKENS' 

[57] EnumeratedType ::= NotationType | Enumeration
[58] NotationType ::= 'NOTATION' S '(' S? Name (S? '|' S? Name)* S? ')' 
[59] Enumeration ::= '(' S? Nmtoken (S? '|' S? Nmtoken)* S? ')' 
*/

	AttDef.skipWhiteSpace();
	
	String attName = AttDef.getNextToken();
	setAttributeName(attName);
	//System.out.println("Attribute - " + attName);

	/* Get the AttType
		The StringType & the TokenizedType are easy to deal with; EnumeratedType is a bit tricky */

	AttDef.skipWhiteSpace();
	String token = AttDef.getNextToken().trim();

	checkAttType(token);
	
	if( isNotationType() ) { //NotationType
		
		AttDef.skipWhiteSpace();
		AttDef.getNextChar(); // '('
		AttDef.skipWhiteSpace();
		//CR: TODO: Read the notations
		
	} else if( isEnumeration() ){ //Enumeration

		setAttrType(Attribute.TYPE_ENUMERATION);

		if( token.length() == 0 || token.charAt(token.length() - 1) != ')') {
			//read until you've gotten all the enums
			char nextChar;
			while( !AttDef.endOfData() && (nextChar = AttDef.getNextChar()) != ')')
				token += nextChar;
			token += ')';
		}

		DTDData enums = new DTDData(token);
		setEnumeration(readEnums(enums));
	}

	readDefaultDecl(AttDef);
}

/**
 * Returns true if this attribute is of type ID.
 * 
 * @return <code>true</code> if the attribute is of
 * type ID; <code>false</code> otherwise.
 */
public boolean isIDType() {
	
	if(getAttType() == AttDef.TYPE_TOKENIZED && getDataType().equals(ID)) {
		return true;
	}
	
	return false;
}
/**
 * Sets the attribute type.
 * 
 * @param newType The attribute type.
 */
private void setAttrType(int newType) {
	attType = newType;
}
/**
 * Set the data type of the attribute.
 * 
 * @see #getDataType
 * 
 * @param newDataType java.lang.String
 */
private void setDataType(String newDataType) {
	dataType = newDataType;
}
/**
 * Set the default declaration of the attribute.
 * 
 * @see #getDefaultValue
 * 
 * @param newDefaultDecl java.lang.String
 */
private void setDefaultValue(String newDefaultDecl) {
	defaultValue = newDefaultDecl;
}
/**
 * Set the permitted values for the attribute (only used 
 * for attributes of the Enumerated type).
 * 
 * @param newEnumeration Enumerated values for the attribute.
 */
private void setEnumeration(String[] newEnumeration) {
	enumeration = newEnumeration;
}
/**
 * Set the name of the attribute.
 * 
 * @param newName java.lang.String
 * 
 * @see #getAttributeName
 */
public void setAttributeName(String newName) {
	name = newName;
}
/**
 * Set the optionality of the attribute.
 * 
 * @see #getOptionality
 * 
 * @param newOptionality java.lang.String
 */
private void setOptionality(String optionalityStr) {
	
	//CR: TODO: Check if it is #REQUIRED instaed of REQUIRED
	//CR: TODO: Do null check
	if(optionalityStr.equals(OPTIONALITY_STR_REQUIRED)) {
		optionality = AttDef.OPTIONALITY_REQUIRED;
	} else if(optionalityStr.equals(OPTIONALITY_STR_IMPLIED)) {
		optionality = AttDef.OPTIONALITY_IMPLIED;
	} else if(optionalityStr.equals(OPTIONALITY_STR_FIXED)) {
		optionality = AttDef.OPTIONALITY_FIXED;
	}
	//CR: TODO: Handle the error case
}
}